Bring the publisher handler back under the complexity law, and report why the law had no teeth - #256
Merged
Merged
Conversation
BardHandler.handle measured 19 against a limit of 10 and was never in
complexipy-snapshot.json: drift, not grandfathered debt, and it landed
because nothing was checking.
Three behaviour-preserving extractions, each one measured so the branches
are removed rather than relocated:
BardHandler._create_branch 6 the branch-creation try/except and its
'already exists' probe
_partial_metadata 3 the salvage-what-we-learned map on the
throw path
_failed 0 the FAILED envelope that four refusal
paths each built by hand
BardHandler.handle 19 -> 7
No branch was deleted as unreachable and none was moved into a helper
that is itself over the limit.
The suite is the control and its counts are unchanged on both sides:
5746 passed, 3 skipped, 37 xfailed, 20 xpassed. mypy adds no new errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
exemptions.json pinned bard.py's BLE001 to line 282. The suppression did not move; the lines above it did, and the gate went red reading UNREGISTERED_SUPPRESSION against a suppression that was registered. A corrected number drifts again on the next insertion, so this anchors to BardHandler.handle instead. The matcher already accepts either form (symbol_or_line == str(line) or symbol_or_line == symbol) and the symbol is unique in the file, so it cannot swallow a second suppression. Computed with the gate's own _symbol_spans/_enclosing_symbol rather than by eye. A pointer repair, not a raise: 52 entries and frozen_count 52, both unchanged, and the diff is one line each way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/bonfire/handlers measured EXACTLY 3250 against a ceiling of 3250, so the package had zero headroom and no behaviour-preserving complexity refactor could land in it at all. The structured entry records what the 30 lines buy, the six alternatives rejected and why, and the finding underneath: every handler file holding an un-grandfathered violation also carries a suppression pinned by line number, so the register that records accepted debt structurally forbids the refactoring that would retire it. Surfaced in the PR body and in the lane report, not taken silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…omplexity-law # Conflicts: # file-budget.json
…omplexity-law # Conflicts: # file-budget.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The refactor in this PR is the smallest thing in it. Four findings come first, because they are what the refactor uncovered and they are worth more than it.
1. A green
complexipyrun silently empties the snapshot to[]and exits 0The 20-entry debt register can be destroyed by a passing run over any clean subtree. Control rod,
A -> B -> A, run in this worktree:A red run leaves it untouched (verified: md5 unchanged after a failing full-
srcrun). Only success destroys it, and it exits 0 while doing so.This is not theoretical. It fired a second time, unprompted, while I was verifying my own fix: a single
complexipy src/bonfire/handlers/bard.pyto confirm the handler was clean wiped the register again. Any developer who fixes one file and checks their work triggers it.The kit's own
docs/tool-spikes.mdstates "The snapshot does NOT auto-shrink." That was written against complexipy 5.5.0. The pinned battery is 5.6.0, where it is false. A register that a passing run can wipe is not a register.2. CI does not run the complexity gate at all
.github/workflows/quality.ymlpins the shared kit atadaa4ebf429fd9ba49de6520d4636e0f8604c71d. Proved three independent ways:cf-sticky-check,cf-file-budget,cf-mirror-check,cf-recursion-check,cf-exemptions, mypy-through-baseline, pytest. No complexipy step. Nocf-import-contractstep. The only occurrence of the word is a comment on the install step, "[dev] carries ruff/mypy/mypy-baseline/pytest/complexipy". CI installs the complexity gauge and never invokes it.gh run view 30366566328 --log(quality-gate, push tomain) contains exactly 17##[group]Run ...step headers. Every one matches the list above. None is complexipy.e842029"...gate complexipy...",3963ce5"cf-import-contract joins the reusable quality gate",2f5a4d5"rewire quality-gate.yml to one cf-gate step".A one-line SHA bump lights up complexipy and
cf-import-contracttogether. That file is outside this lane's scope and is untouched here.Worth stating plainly: this PR, which fixes a complexity violation, will be graded by a CI that does not measure complexity.
3. Even wired, the gate grades at 15, not at the repo's law of 10
cf-gateinvokescomplexipy <source_root>with no--max-complexity-allowed, so it inherits complexipy's default of 15. Measured consequence in this one package: three functions sit over the law and under the gate and would pass a fully-wired CI silently.steward.py::StewardHandler::handlesage_correction_bounce.py::...::_run_correction_cyclepreflight_pytest.py::parse_pytest_junit_xmlThis is why the drift was reported as two functions. There are five un-grandfathered violations in
src/bonfire/handlers, not two -- the other three were invisible at threshold 15.4. The registers structurally forbid the refactoring the law requires
Every handler file holding an un-grandfathered violation also carries a suppression that
exemptions.jsonpinned by line number:Refactoring any of those functions shifts a pinned line and turns
cf-exemptionsred. The register that records accepted debt structurally forbids the refactoring that would retire it. That is the mechanism by which this debt never moves, and it explains the grandfather list better than any story about priorities.This PR re-anchors only its own row, to a symbol. The other eight are still line-pinned.
The refactor
BardHandler.handlemeasured 19 against a limit of 10 and was never incomplexipy-snapshot.json-- drift, not grandfathered debt, landed because nothing was checking.Three behaviour-preserving extractions. Every function created is measured, so the branches are removed rather than relocated:
BardHandler::handleBardHandler::_create_branch(new)_partial_metadata(new)_failed(new)_slugify_task,__init__complexipy src/bonfire/handlers/bard.py --max-complexity-allowed 10reports "All functions are within the allowed complexity."No branch was deleted as unreachable. No branch was moved into a helper that is itself over the limit.
Control rod: the suite is the control, and its counts do not move
Every count identical. mypy adds 0 new errors over the baseline.
Gate board
7bddd71Identical to bare main -- this branch adds no red. Both remaining reds are pre-existing on
7bddd71and are stated honestly rather than claimed as wins:new: 0, unresolved: 54-- baseline entries that no longer reproduce, not new errors. It is red on bare main locally while CI is green on that same commit, so it is an artefact of the local kit checkout being newer than the pinned one. Not mine, not diagnosed here.wizard.py::WizardHandler::handle(17), left untouched on purpose.Why
wizard.pyis untouchedBlocked three ways before the first edit: 499 lines against a 500-line new-file cap, in a package that had zero budget headroom, with its own suppression pinned at line 492. The prior handlers raise already recorded that the next change to it must split the module rather than compress it; a split creates a new file that draws against the same package total. It needs its own lane and its own budget decision.
architect.py::ArchitectHandler::handle(28) is grandfathered and was left alone. Nothing fell out of this work that reduced it.The two register edits, surfaced not smuggled
exemptions.json-- a pointer repair, not a raise.bard.py's BLE001 pin moved from"282"to"BardHandler.handle". The suppression never moved; the lines above it did. A corrected number would drift again on the very next insertion, which is the defect itself re-committed, so this anchors to the symbol. The matcher already accepts either form (symbol_or_line == str(line) or symbol_or_line == symbol), and the symbol was computed with the gate's own_symbol_spans/_enclosing_symboland confirmed unique in the file so it cannot swallow a second suppression. Diff is one line each way; entries 52,frozen_count52, both unchanged; no escaped em-dashes.file-budget.json--src/bonfire/handlers3250 to 3280, 30 lines. The package measured exactly 3250 against a ceiling of 3250: zero headroom, so no behaviour-preserving complexity refactor could land in it at all. The structuredpackage_raisesentry carries what the lines buy and the six alternatives rejected, including: adding the function tocomplexipy-snapshot.json(the exact move this programme exists to end); the 7-line minimum variant (measured for complexity and lines but never run against the suite -- an unproven refactor is not cheaper, it is unmeasured); deleting comments to buy the lines back (gaming, and futile since the gauge measuresmax(physical, statements)); and apurposeentry for a split file, which would have made the package total fall but drops a live file out of the ratchet permanently.Three commits, deliberately separate: the refactor, the re-anchor, the raise.
No entry was added to
complexipy-snapshot.json. It is byte-identical to7bddd71(md5ce1dbdc0ff983e828e4a9ba69998fe67).Recommendations -- reported, not acted on
--max-complexity-allowed 10so it grades the law rather than complexipy's default.docs/tool-spikes.md, which documents the 5.5.0 behaviour as current.986fc03-- "Grandfathers existing type errors (43) and complexity at gate-mount... Ratchet forward." Stated intent, named owner. But no entry has ever left the list in the five weeks since, there is no doc, comment or README in this repo describing how one leaves (greppeddocs/,CLAUDE.md,CONTRIBUTING.md,README.md: zero hits forcomplexipy), and there is no mechanism:cf-gateruns complexipy without--snapshot-createand CI never commits, so improvements are never locked in. Booted as a debt register with a plan; operating as an oubliette.What this PR does not prove
cf_qualityis editable-installed from a kit checkout newer than the pin. The finding above rests on the pinned YAML, a real run log and kit ancestry, not on this -- but the local gate board is not byte-for-byte what CI prints._run_correction_cycle14,parse_pytest_junit_xml11) were measured and reported, never touched.quality-gateis actually required in branch protection is invisible from a run log. A gate that runs and is not required still merges red.One observation outside this lane's scope, stated once:
exemptions.jsoncarries internal tracker IDs in itsapproverfields on a public repo. Pre-existing across many rows; fixing it would blow up the diff and race two live lanes.🤖 Generated with Claude Code